﻿using Common.FrontEnd;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Util.Props;

namespace FrontEnd.Transform
{
    /// <summary>
    /// Applies a logarithm and then a Discrete Cosine Transform (DCT) to the input data. The input data is normally the mel
    /// spectrum. It has been proven that, for a sequence of real numbers, the discrete cosine transform is equivalent to the
    /// discrete Fourier transform. Therefore, this class corresponds to the last stage of converting a signal to cepstra,
    /// defined as the inverse Fourier transform of the logarithm of the Fourier transform of a signal. The property {@link
    /// #PROP_CEPSTRUM_LENGTH}refers to the dimensionality of the coefficients that are actually returned, defaulting to
    /// 13. When the input is mel-spectrum, the vector returned is the MFCC (Mel-Frequency
    /// Cepstral Coefficient) vector, where the 0-th element is the energy value.
    /// </summary>
    public class DiscreteCosineTransform: BaseDataProcessor
    {
        /** The property for the number of filters in the filterbank. */
        [S4Integer(defaultValue = 40)]
        public static String PROP_NUMBER_FILTERS = "numberFilters";

        /** The property for the size of the cepstrum */
        [S4Integer(defaultValue = 13)]
        public static String PROP_CEPSTRUM_LENGTH = "cepstrumLength";

        protected int cepstrumSize; // size of a Cepstrum
        protected int numberMelFilters; // number of mel-filters
        protected double[][] melcosine;


        public DiscreteCosineTransform( int numberMelFilters, int cepstrumSize ) 
        {
            this.numberMelFilters = numberMelFilters;
            this.cepstrumSize = cepstrumSize;
        }

        public DiscreteCosineTransform( ) {
        }

        override
        public void newProperties(PropertySheet ps)
        {
            base.newProperties(ps);

            numberMelFilters = ps.getInt(PROP_NUMBER_FILTERS);
            cepstrumSize = ps.getInt(PROP_CEPSTRUM_LENGTH);
        }

        /// <summary>
        /// 
        /// </summary>
        override public void initialize() 
        {
            base.initialize();
        }

        /// <summary>
        /// Returns the next DoubleData object, which is the mel cepstrum of the input frame. Signals are returned
        /// unmodified.
        /// </summary>
        /// <returns>the next available DoubleData melcepstrum, or Signal object, or null if no Data is available</returns>
        override public IData getData()
        {
            IData input = getPredecessor().getData(); // get the spectrum
            getTimer().start();
            if (input != null && input is DoubleData) 
            {
                input = process((DoubleData) input);
            }
            getTimer().stop();
            return input;
        }

        static double LOG_FLOOR = 1e-4;
    
        /// <summary>
        /// Process data, creating the mel cepstrum from an input spectrum frame.
        /// </summary>
        /// <param name="input">a MelSpectrum frame</param>
        /// <returns> mel Cepstrum frame</returns>
        private DoubleData process(DoubleData input)
        {

            double[] melspectrum = input.Values;

            if (melcosine == null) 
            {
                numberMelFilters = melspectrum.Length;
                computeMelCosine();

            } 
            else if (melspectrum.Length != numberMelFilters) 
            {
                throw new ArgumentException("MelSpectrum size is incorrect: melspectrum.length == " +
                                melspectrum.Length + ", numberMelFilters == " +
                                numberMelFilters);
            }
            // first compute the log of the spectrum
            for (int i = 0; i < melspectrum.Length; ++i) 
            {
                melspectrum[i] = Math.Log(melspectrum[i] + LOG_FLOOR);
            }

            double[] cepstrum;

            // create the cepstrum by apply the melcosine filter
            cepstrum = applyMelCosine(melspectrum);

            return new DoubleData(cepstrum, input.getSampleRate(),
                    input.getFirstSampleNumber());
        }


        /// <summary>
        /// Compute the MelCosine filter bank. 
        /// </summary>
        protected void computeMelCosine() 
        {
            melcosine = new double[cepstrumSize][/**/];
            for (int i = 0; i < cepstrumSize; i++)
                melcosine[i] = new double[numberMelFilters];

            double period = (double) 2 * numberMelFilters;
            for (int i = 0; i < cepstrumSize; i++) 
            {
                double frequency = 2 * Math.PI * i / period;
                for (int j = 0; j < numberMelFilters; j++) 
                {
                    melcosine[i][j] = Math.Cos(frequency * (j + 0.5));
                }
            }
        }


        /**
         * Apply the MelCosine filter to the given melspectrum.
         *
         * @param melspectrum the MelSpectrum data
         * @return MelCepstrum data produced by apply the MelCosine filter to the MelSpectrum data
         */
        protected double[] applyMelCosine(double[] melspectrum) 
        {
            // create the cepstrum
            double[] cepstrum = new double[cepstrumSize];
            double period = numberMelFilters;
            double beta = 0.5;
            // apply the melcosine filter
            for (int i = 0; i < cepstrum.Length; i++) 
            {
                if (numberMelFilters > 0) {
                    double[] melcosine_i = melcosine[i];
                    int j = 0;
                    cepstrum[i] += (beta * melspectrum[j] * melcosine_i[j]);
                    for (j = 1; j < numberMelFilters; j++) {
                        cepstrum[i] += (melspectrum[j] * melcosine_i[j]);
                    }
                    cepstrum[i] /= period;
                }
            }
        
            return cepstrum;
        }
    }
}
